Professional software can mean commercial software, but it can also mean high quality software, including freeware. One of the features you'll find in the best software is the program's ability to remember user preferences.
Saving User Preferences
There are several ways to save a user's preferences. Have a look at the code for Freeform, the GUI designer that is packaged with Liberty BASIC. It uses a sequential text file to store user preferences. When Freeform opens, it reads these values in a loop from the file and puts them into operation during the program.
Many commercial programs store user preferences in the Windows registry. This method of storage serves to bloat the registry, and it also leaves room for critical errors. A corrupted registry can cause problems, and it can even cause Windows to fail.
The easiest way to store user preferences is in an initialization file. This is often called an "ini" file. Windows gives us a really easy way to create and read ini files.
Ini files by API
Two functions handle ini files, WritePrivateProfileStringA and GetPrivateProfileStringA. These functions were discussed in issue #102 of the newsletter. These files are plain text files, just like the ini file for Freeform. There are a couple of differences between API ini files and sequential text files.
Differences: API versus sequential text files
Unless a path is specified when writing an API ini file, it is created in the system folder. This serves to hide it from the user, so he can't accidentally corrupt it. If no path is specified when creating a sequential file with the OPEN command, it is created in the program's directory.
Sequential files must be read sequentially, from beginning to end. You cannot extract information from the middle of the file. API ini files allow you to extract a single item from any place in the file. They also let you organize the information in groups and give labels to the various items. This makes it very easy to retrieve any piece of data. There is one other really nice feature. The function that retrieves data from the ini file allows you to specify a default value for that item.
Sequential files must exist or the "open" command halts the program with an error. API ini files are created automatically if they don't exist.
API ini files allow you to add values easily. If you use a sequential file and you want to add options, you must take that into account when reading the file, and continually check for the end of the file with EOF() before attempting to read a new line. You cannot easily rearrange the ini file, either. That's why we've been creating a new ini file with a new filename for each version of Eddie. We won't need to do that any longer. Since we are now using an API ini file, we can add data any way we want and it's easy to read it back without worrying about reaching the end of the file or creating a disorganized mess.
Parts of the Ini File
The API calls require a section name for each data item. This section name can be anything you want. You might divide your ini files into sections like "recent files", "options", and "password info". You can use a single section if you want, names something like "my cool program". The choice is yours.
You'll also need a key name. This is a label for the information. For example, if there were an option to display a grid, you might have a key name of "grid on".
Each key has a value. For the key "grid on" you might have values of either "yes" or "no". Simple, isn't it?
You'll also need to specify the filename for the ini file. Try to make it a unique name so that it doesn't conflict with ini files from other apps. You might want to use the name of your own application in the ini filename, like "mycoolprog.ini" or something similar.
When retrieving data from the ini file with GetPrivateProfileStringA, you send a default key value into the call. If no value is found for that key, your default value is returned to you. You might set the default value for "grid on" to be "yes" for instance. After making the call, you'll always have a value for each bit of initialization data, so you won't need to add additional code, like
if gridOn$ = "" then gridOn$ = "yes"
Syntax
The syntax for the functions to read and write ini files can be found at the end of this article. It includes Liberty BASIC code samples, and the documentation from Microsoft.
Demo
What follows is a small demonstration program that saves the size and location of the program window, so that it opens at those same coordinates the next time a user runs the program. The demo requires a couple of API calls in addition to the ini file functions. See the API Corner for details on these API calls.
The IsZoomed api call returns true if the window whose handle is passed into the call is minimized. If it is minimized, our program doesn't attempt to save the coordinates. If it is not minimized the program uses the GetWindowRect API call to retrieve the coordinates of the upper left and lower right corner of the window. They are placed in a Rect struct. The x1 value of the struct is the UpperLeftX coordinate. The y1 value is the UpperLeftY coordinate. To get the width, we subtract the x1 coordinate (left x) from the x2 coordinate (right x) and we do the same with the y coordinates to get the height. We then write these values to the ini file with WritePrivateProfileStringA.
When the program opens, we use the GetPrivateProfileStringA call to retrieve the last used coordinates and size of the window and set those values before opening our program window.
Demo Code
'this sub reads the ini file and extracts
'location and size coordinates of window
'defaults are used if no entries are found
gosub [getCoords]
nomainwin
open "My Program" for window as #1
#1 "trapclose [quit]"
wait
[quit]
handle = hwnd(#1)
calldll #user32, "IsIconic",_
handle as ulong,_ 'handle of window
result as boolean 'returns true if window is minimized
if not(result) then
struct Rect, x1 As Long, y1 As Long, x2 As Long, y2 As Long
CallDLL #user32, "GetWindowRect",handle as uLong,Rect As struct, result As Long
ulx = Rect.x1.struct : uly = Rect.y1.struct
width = Rect.x2.struct - ulx
height = Rect.y2.struct - uly
'write location and size of window to ini file
call WriteIniFile "My Program", "ULX", str$(ulx), "myapp.ini"
call WriteIniFile "My Program", "ULY", str$(uly), "myapp.ini"
call WriteIniFile "My Program", "WIDTH", str$(width), "myapp.ini"
call WriteIniFile "My Program", "HEIGHT", str$(height), "myapp.ini"
end if
close #1:end
Sub WriteIniFile lpAppName$, lpKeyName$, lpString$, lpFileName$
CallDLL #kernel32, "WritePrivateProfileStringA", _
lpAppName$ As ptr, _ 'section name
lpKeyName$ As ptr, _ 'key name
lpString$ As ptr, _ 'key value
lpFileName$ As ptr, _ 'ini filename
result As boolean 'nonzero = success
end sub
[getCoords]
UpperLeftX = val(GetIniFile$("My Program","ULX","10","myapp.ini"))
UpperLeftY = val(GetIniFile$("My Program","ULY","10","myapp.ini"))
WindowWidth = val(GetIniFile$("My Program","WIDTH","300","myapp.ini"))
WindowHeight = val(GetIniFile$("My Program","HEIGHT","200","myapp.ini"))
return
Function GetIniFile$(lpAppName$, lpKeyName$,lpDefault$,lpFileName$)
nSize=100
lpReturnedString$=Space$(nSize)+Chr$(0)
CallDLL #kernel32, "GetPrivateProfileStringA", _
lpAppName$ As ptr, _'section name
lpKeyName$ As ptr, _'key name
lpDefault$ As ptr, _'default string returned if there is no entry
lpReturnedString$ As ptr, _ 'destination buffer
nSize As long, _ 'size of destination buffer
lpFileName$ As ptr, _ 'ini filename
result As ulong 'number of characters copied to buffer
GetIniFile$=Left$(lpReturnedString$,result)
end function
WritePrivateProfileStringA
CallDLL #kernel32, "WritePrivateProfileStringA", _
lpAppName$ As ptr, _ 'section name
lpKeyName$ As ptr, _ 'key name
lpString$ As ptr, _ 'key value
lpFileName$ As ptr, _ 'ini filename
result As boolean 'nonzero = success
lpAppName
Points to a null-terminated string containing the name of the section to which the string will be copied. If the section does not exist, it is created. The name of the section is case-independent; the string can be any combination of uppercase and lowercase letters.
lpKeyName
Points to the null-terminated string containing the name of the key to be associated with a string. If the key does not exist in the specified section, it is created. If this parameter is NULL, the entire section, including all entries within the section, is deleted.
lpString
Points to a null-terminated string to be written to the file. If this parameter is NULL, the key pointed to by the lpKeyName parameter is deleted.
Windows 95: This platform does not support the use of the TAB (\t) character as part of this parameter.
lpFileName
Points to a null-terminated string that names the initialization file.
GetPrivateProfileStringA
CallDLL #kernel32, "GetPrivateProfileStringA", _
lpAppName$ As ptr, _'section name
lpKeyName$ As ptr, _'key name
lpDefault$ As ptr, _'default string returned if there is no entry
lpReturnedString$ As ptr, _ 'destination buffer
nSize As long, _ 'size of destination buffer
lpFileName$ As ptr, _ 'ini filename
result As ulong 'number of characters copied to buffer
lpAppName
Points to a null-terminated string that specifies the section containing the key name. If this parameter is NULL, the GetPrivateProfileString function copies all section names in the file to the supplied buffer.
lpKeyName
Pointer to the null-terminated string containing the key name whose associated string is to be retrieved. If this parameter is NULL, all key names in the section specified by the lpAppName parameter are copied to the buffer specified by the lpReturnedString parameter.
lpDefault
Pointer to a null-terminated default string. If the lpKeyName key cannot be found in the initialization file, GetPrivateProfileString copies the default string to the lpReturnedString buffer. This parameter cannot be NULL.
lpReturnedString
Pointer to the buffer that receives the retrieved string.
nSize
Specifies the size, in characters, of the buffer pointed to by the lpReturnedString parameter.
lpFileName
Pointer to a null-terminated string that names the initialization file. If this parameter does not contain a full path to the file, Windows searches for the file in the Windows directory.